#!/usr/bin/perl
#
# This handles the stepwise explanation of the determinantal solution 
#
# 2009, Mike Widner <mikewidner@gmail.com>
#
#########

use strict;
use warnings;
use lib qw( /home/mwidner/workspace/Fangcheng );

use Data::Dumper;
use Carp;
use Fangcheng::Matrix;
use Fangcheng::DB;
use Fangcheng::CGI;
use Fangcheng::CGI::Display;
use Fangcheng::CGI::UserInput;
use Fangcheng::Calc::Determinants;
use Fangcheng::LaTeX;
use Fangcheng::LaTeX::Formula;
use Fangcheng::LaTeX::CountingBoard;
use Fangcheng::LaTeX::CountingBoard::Arrows;

##### Global variable and object definitions #####

my $RAND_MAX	= 1000;	# maximum value for randomly filled arrays
my $database	= '/home/mwidner/workspace/Fangcheng/Matrices.db';
my $MATHTEX		= 'http://uts.cc.utexas.edu/~rhart/cgi-bin/mathtex.cgi?';
my $calc		= Fangcheng::Calc::Determinants->new();
my $cgi			= Fangcheng::CGI::Display->new( 'Chinese Linear Algebra by Steps' );
my $user		= Fangcheng::CGI::UserInput->new();
my $latex		= Fangcheng::LaTeX->new();
my $board		= Fangcheng::LaTeX::CountingBoard->new();
my $arrow		= Fangcheng::LaTeX::CountingBoard::Arrows->new();
my $formula		= Fangcheng::LaTeX::Formula->new();
																
$cgi->session->expire('1m');

# handles what do at various steps in the process
# avoids long if-elsif structure
my %dispatch = ('NO_MATRIX'		=> \&chooseMatrix,
				'CHOOSE_MATRIX'	=> {
					'book'			=> \&loadDB,
					'user'			=> \&defineMatrix,	
				},
				'SOLVE_MATRIX'	=> \&solveMatrix
			);

# what types of matrices the user can choose
# the numbers allow us to sort the keys easily
# keys must match keys in %dispatch
my %matrixType = (	'book' => 'Matrix from Book', 
					'user' => 'User-defined', 
				 );

####### Control Logic ########

print $cgi->startPage();		# important for printing headers, getting cookies

if ( defined $cgi->param( 'RESET' ) ) {
	$cgi->resetSession();
	$dispatch{'NO_MATRIX'}->();
}
elsif ( defined $cgi->param( 'NEW_MATRIX' ) ) {
	my $matrix = $cgi->param( 'MATRIX_DB' )->get( $cgi->param( 'MATRIX_NUM' ) );
	$cgi->param( 'MATRIX_TO_SOLVE', $matrix );
	recalculate( $matrix );
	solveMatrix();
}
elsif ( not defined $cgi->param( 'CHOOSE_MATRIX' ) ) {
	# first choose a matrix type
	$dispatch{'NO_MATRIX'}->();
}
elsif ( defined $cgi->param( 'CHOOSE_MATRIX' ) and not defined $cgi->param( 'SOLVE_MATRIX' ) ) {
	# next choose a specific matrix
	$dispatch{'CHOOSE_MATRIX'}->{ $cgi->param( 'CHOOSE_MATRIX' ) }->();
}
elsif ( defined $cgi->param( 'SOLVE_MATRIX' ) ) {
	# now we're into the solving itself; just step through
	$dispatch{'SOLVE_MATRIX'}->();
}
else {
	croak "I don't know what to do";	# put a better error handler in
}

print $cgi->resetButton();

print $cgi->endPage();		# saves state
exit(0);

###### Functions called by dispatch table, which handle actual work ######
###### NOTE: we make use of global objects in these functions       ######
sub chooseMatrix {
	print $cgi->query->h3( 'Choose matrix type to solve:' );
	print $cgi->query->start_form();
	print $cgi->query->popup_menu(	-name	=> 'CHOOSE_MATRIX',
									-values	=>	[ sort keys %matrixType ],
									-default=> undef,
									-labels	=> \%matrixType );
	print $cgi->query->p;
	print $cgi->query->submit();
	print $cgi->query->end_form();
}

# load matrices from database; user chooses one
sub loadDB {
	if ( not defined $cgi->query->param( 'MATRIX_NUM' ) ) {
		my $matrixDB =  Fangcheng::DB->new($database); # load predefined matrices
		# when endPage() called, will save CGI object to session, including entire matrix db
		$cgi->param( 'MATRIX_DB', $matrixDB );
		print $cgi->query->h3( 'Choose a matrix: ' );
		print $cgi->query->start_form();
		print $cgi->query->popup_menu(	-name	=>	'MATRIX_NUM',
										-values	=>	$matrixDB->list()	);
		print $cgi->query->p;
		print $cgi->query->submit();
		print $cgi->query->end_form();
	} 
	else {
		croak "We already chose a matrix";
	}
	$cgi->param( 'SOLVE_MATRIX', 1 );	# we're into the solving portion after this
}

# set user-defined matrix of size N
sub defineMatrix {
	if (not defined $cgi->param( 'MATRIX_SIZE' ) ) {
		print $cgi->query->start_form();
		print $cgi->query->h3( 'Matrix of size N:' );
		print $cgi->query->textfield( -name	=> 'MATRIX_SIZE',
										-size	=> 2 );
		print $cgi->query->p();
		print $cgi->query->h3( 'Fill matrix with: ' );
		print $cgi->query->popup_menu(-name	=> 'MATRIX_TYPE',
										-values	=> ['rand', '0', '1'],
										-labels	=> {'rand'	=> 'Random values',
													'0'		=> 'Zeroes',
													'1'		=> 'Ones'	});
		print $cgi->query->p();
		print $cgi->query->submit();
		print $cgi->query->end_form();
	}
	else {
		my $matrix;
		my $init = $cgi->param( 'MATRIX_TYPE' );
		if ($init eq 'rand') {
			# relying on the fact that ::Matrix takes 'rand' as an option when new
			$matrix = Fangcheng::Matrix->new( $cgi->param( 'MATRIX_SIZE' ), $init, $RAND_MAX );		
		}
		else {
			$matrix = Fangcheng::Matrix->new( $cgi->param( 'MATRIX_SIZE' ), $init );
		}	
		$matrix = $user->formDeterminantMatrix( $matrix );
		displayTable( $matrix );
		print $cgi->query->start_form();
		print $cgi->query->submit('Solve');
		print $cgi->query->end_form();
		$cgi->param( 'MATRIX_TO_SOLVE', $matrix );	# we're into the solving portion after this
		$cgi->param( 'MATRIX_DB', Fangcheng::DB->new( $database ) );
		$cgi->param( 'SOLVE_MATRIX', 1 );
	}
}

##### State handling functions #####
sub getMatrix {
	my $matrix = $cgi->param( 'MATRIX_TO_SOLVE' );
	if (not defined $matrix ) {
		if ( defined $cgi->param( 'MATRIX_NUM' ) ) {
			$matrix = $cgi->param( 'MATRIX_DB' )->get( $cgi->param( 'MATRIX_NUM' ) ) or $cgi->error("No matrix found!");
		}
		else {
			$cgi->error("I have no matrix to solve.");
		}
	}
	return( $matrix );
}

sub getState {	
	my ($matrix) = shift;
	my ($solved, $step);
	if ( not defined $cgi->param( 'SOLVED' ) ) {
		$cgi->param( 'STEP', 0 );
		$solved = $calc->solve( $matrix );
	}
	else {
		$solved 	= $cgi->param( 'SOLVED' );
		$calc 		= $cgi->param( 'CALC' );
		$formula 	= $cgi->param( 'FORMULA' );
		$arrow 		= $cgi->param( 'ARROW' );
		$board		= $cgi->param( 'BOARD' );
	}
	$step = $cgi->param( 'STEP' );
	return( $solved, $calc, $formula, $arrow, $board, $step );
}

sub saveState {
	my ($solved, $matrix) = @_;
	$cgi->param( 'CALC', $calc );
	$cgi->param( 'FORMULA', $formula );
	$cgi->param( 'ARROW', $arrow );
	$cgi->param( 'BOARD', $board );
	
	if ( defined $solved and defined $matrix ) {
		$cgi->param( 'SOLVED', $solved );
		$cgi->param( 'MATRIX_TO_SOLVE', $matrix );
	}
}

# reset the LaTeX objects
sub resetLatex {
 	$arrow		= Fangcheng::LaTeX::CountingBoard::Arrows->new();
	$formula 	= Fangcheng::LaTeX::Formula->new();
	$board 		= Fangcheng::LaTeX::CountingBoard->new();
	saveState();
}

##### forms and display handling functions #####
sub listProblems {
	print 'Problem: ';
	print $cgi->query->popup_menu(	-name	=>	'MATRIX_NUM',
									-values	=>	$cgi->param('MATRIX_DB')->list()	);
	print $cgi->query->p;
	print $cgi->query->submit(	-name => 'NEW_MATRIX',
								-value=> 'Solve a different matrix' );
	print $cgi->query->hr;
	print $cgi->query->p;
}

sub displayTable {
	my ($curMatrix) = shift;
	print $cgi->matrixTable( $curMatrix );
	print $cgi->query->p;
	print $cgi->query->submit( -name => 'RECALC',
							   -value => 'Recalculate' );
	print $cgi->query->p;
}

sub displayLatex {
	my ($curMatrix, $prevMatrix, $solved, $step) = @_;
	print $cgi->query->hr;							   
	print $cgi->query->h3("Step: $step");
	print $cgi->query->p;
	
	if ( $step == 0 ) {
		print $board->description;
		print $cgi->query->p;
		displayImage( $board );
	}
	elsif ( defined $arrow->execute( $curMatrix, $prevMatrix, $board, $calc, $step ) ) {
		print $arrow->description;
		print $cgi->query->p;
		displayImage( $arrow );		
	}

	if ( $step < $solved->total ) {
		print "This corresponds to the following augmented matrix:";
		print $cgi->query->p;
		print $cgi->query->img( { -src=> $MATHTEX . $latex->matrix( $curMatrix ), -class => 'matrix' } ); 
		print $cgi->query->p;
	}
	
	foreach my $str ( $formula->toString() ) {
		if ( $calc->getStage( $step ) <= 1 ) {
			print "Using variables, we can show what this corresponds to in terms of an exemplar:";
			print $cgi->query->p;
		}
		print $cgi->query->img( { -src=> $MATHTEX . $str, -class => 'matrix' } );		
		print $cgi->query->p;	
	}
}

sub chooseStep {
	my ($solved, $step) = @_;
	print $cgi->query->submit( -name => 'NEXT_STEP', -value => 'Choose Step' );
	$cgi->query->delete( 'STEP' );
	print $cgi->query->popup_menu(	-name	=> 'STEP',
									-values	=> [( 0 .. $solved->total )],
									-default=> $step + 1 );
	$cgi->param( 'LAST_STEP', $step );
}

sub imageURL {
	my ($str) = shift;
	return( $cgi->query->img( { -src=> $MATHTEX . $str, -class => 'matrix' } ) );	
}

sub displayImage {
	my ($object) = shift;
	my $ret = $object->toString();
	if ( ref($ret) eq 'ARRAY' ) {
		foreach my $str ( @$ret ) {
			print imageURL( $str );
		}
	}
	elsif ( defined $ret ) {
		print imageURL( $ret );
	}
	print $cgi->query->p;			
}

##### functions for handling control logic #####

# step backwards in calculation
sub stepBack {
	my ($solved, $step) = @_;

	# it's easiest just to start over with clean objects if we're going backwards
	# so we reset the global variables with new objects
	resetLatex();
	foreach my $backStep ( 0 .. $step ) {
		my $matrix = $solved->get( $backStep );
		$board->execute( $matrix );
		$formula->execute( $matrix, $calc, $backStep );
		if ( $backStep > 0 ) {
			my $prev = $solved->get( $backStep - 1 );
			$arrow->execute( $matrix, $prev, $board, $calc, $backStep );
		}			
	}
}

# when we've skipped step(s) forward, we need to step through the missing ones
sub stepForward {
	my ($solved, $step, $lastStep) = @_;
	foreach my $i ( 0 .. ( $step - $lastStep - 1 ) ) {
		# populate the objects with values for each step we skipped
		my $matrix = $solved->get( $lastStep + $i );
		$board->execute( $matrix );
		$formula->execute( $matrix, $calc, $lastStep + $i );
		if ( $lastStep > 1 ) {
			my $prev = $solved->get( $lastStep + $i - 1);
			$arrow->execute( $matrix, $prev, $board, $calc, $lastStep + $i )			
		}
	}			
}

sub jumpToStep {
	my ($solved, $step) = @_; 

	my $lastStep = $cgi->param( 'LAST_STEP' );
	return if not defined( $lastStep );
	# stepped backwards
	if ( $step < $lastStep ) {
		stepBack( $solved, $step );
	}
	# jumped forwards
	elsif ( $step - $lastStep > 1 ) {
		stepForward( $solved, $step, $lastStep );
	}
}

sub recalculate {
	my ( $matrix ) = shift;
	my $solved = $calc->solve( $matrix );
	$cgi->clear( 'STEP' );
	$cgi->param( 'STEP', 0 );	# reset to step 0
	$cgi->param( 'SOLVED', $solved );
	$cgi->clear( 'LAST_STEP' );
	$cgi->clear( 'RECALC' );
	resetLatex();
	jumpToStep( $solved, 0 );
}

##### control function for stepping through solution #####
sub solveMatrix {
	print $cgi->query->start_form();
	my $matrix = getMatrix();	
		
	if ( defined $cgi->param( 'RECALC' ) ) {
		$matrix = $cgi->userMatrix( $matrix );
		recalculate( $matrix );
	}
	
	# get state
	my ($solved, $calc, $formula, $arrow, $board, $step) = getState( $matrix );

	# if we incremented by 1, this function does nothing
	jumpToStep( $solved, $step );

	# give problem list
#	listProblems();

	# get current and previous matrices
	my $curMatrix = $solved->get( $step );
	my $prevMatrix;
	if ( $step > 0 ) {
		$prevMatrix = $solved->get( $step - 1);
	}

	# execute and display results
	$formula->execute( $curMatrix, $calc, $step );
	$board->execute( $curMatrix );
	displayTable( $curMatrix );
	displayLatex( $curMatrix, $prevMatrix, $solved, $step );
	
	# finish up
	chooseStep( $solved, $step );	
	print $cgi->query->end_form();	
	saveState( $solved, $matrix );
}
